// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';

import 'package:dart_style/dart_style.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
import 'package:yaml/yaml.dart';

import 'util.dart';

class FunctionConfig {
  /// The number of variable arguments
  final int? varArgs;

  const FunctionConfig({this.varArgs});
}

abstract interface class Config {
  /// The name for the configuration
  String? get name;

  /// The description for the configuration
  String? get description;

  /// Preamble to add at the top of generated code
  String? get preamble;

  /// The input files
  List<String> get input;

  /// The output file or directory to write bindings to
  String get output;

  /// The configuration file
  Uri? get filename;

  /// The Dart Language Version to use
  Version get languageVersion;

  /// Function configuration
  FunctionConfig? get functions;

  bool get singleFileOutput => input.length == 1;

  /// Include the following declarations when generating JS interop code
  ///
  /// This could be a plain name for a declaration, or a [RegExp] pattern
  /// If empty, all declarations will be generated by default
  List<String> get includedDeclarations;

  /// An object consisting of TS Configurations from a tsconfig.json file
  /// used for configuring the TypeScript Program/Compiler
  Map<String, dynamic>? get tsConfig;

  /// The TS Configuration file (tsconfig.json) if any
  String? get tsConfigFile;

  /// Whether to ignore source code warnings and errors
  /// (they will still be printed)
  bool get ignoreErrors;

  /// Whether to generate code for all declarations, including non-exported
  /// declarations
  bool get generateAll;

  /// Treat any unsupported/unimplemented types/declarations as errors
  ///
  /// This is to be used for development purposes only and is not supported
  /// as a config option in the configuration file
  bool get strictUnsupported;

  factory Config(
      {required List<String> input,
      required String output,
      required Version languageVersion,
      FunctionConfig? functions,
      Map<String, dynamic>? tsConfig,
      List<String> includedDeclarations,
      bool generateAll,
      bool ignoreErrors,
      String? tsConfigFile,
      bool strictUnsupported}) = ConfigImpl._;
}

class ConfigImpl implements Config {
  @override
  String? description;

  @override
  Uri? filename;

  @override
  List<String> input;

  @override
  String? name;

  @override
  String output;

  @override
  Version languageVersion;

  @override
  String? preamble;

  @override
  FunctionConfig? functions;

  @override
  List<String> includedDeclarations;

  @override
  Map<String, dynamic>? tsConfig;

  @override
  String? tsConfigFile;

  @override
  bool ignoreErrors;

  @override
  bool generateAll;

  @override
  bool strictUnsupported;

  ConfigImpl._(
      {required this.input,
      required this.output,
      required this.languageVersion,
      this.functions,
      this.tsConfig,
      this.includedDeclarations = const [],
      this.ignoreErrors = false,
      this.generateAll = false,
      this.tsConfigFile,
      this.strictUnsupported = false});

  @override
  bool get singleFileOutput => input.length == 1;
}

class YamlConfig implements Config {
  @override
  Uri filename;

  @override
  List<String> input;

  @override
  String? description;

  @override
  String? name;

  @override
  String output;

  @override
  bool get singleFileOutput => input.length == 1;

  @override
  Version languageVersion;

  @override
  String? preamble;

  @override
  FunctionConfig? functions;

  @override
  List<String> includedDeclarations;

  @override
  Map<String, dynamic>? tsConfig;

  @override
  String? tsConfigFile;

  @override
  bool ignoreErrors;

  @override
  bool generateAll;

  @override
  bool get strictUnsupported => false;

  YamlConfig._(
      {required this.filename,
      required this.input,
      required this.output,
      this.description,
      this.name,
      this.preamble,
      this.functions,
      this.includedDeclarations = const [],
      this.tsConfig,
      this.tsConfigFile,
      String? languageVersion,
      this.ignoreErrors = false,
      this.generateAll = false})
      : languageVersion = languageVersion == null
            ? DartFormatter.latestLanguageVersion
            : Version.parse(languageVersion);

  factory YamlConfig.fromYaml(YamlMap yaml,
      {required String filename, List<String>? input, String? output}) {
    List<String> inputFiles;
    final yamlInput = yaml['input'];
    if (yamlInput is YamlList) {
      inputFiles =
          yamlInput.map((y) => y is String ? y : y.toString()).toList();
    } else if (yamlInput is String) {
      inputFiles = [yamlInput];
    } else if (input != null) {
      inputFiles = input;
    } else {
      throw TypeError();
    }

    final allFiles =
        expandGlobs(inputFiles, extension: '.d.ts', cwd: p.dirname(filename));

    final tsConfig = yaml['ts_config'] as YamlMap?;

    return YamlConfig._(
        filename: Uri.file(filename),
        input:
            allFiles.map((file) => p.join(p.dirname(filename), file)).toList(),
        output:
            p.join(p.dirname(filename), (yaml['output'] ?? output) as String),
        name: yaml['name'] as String?,
        description: yaml['description'] as String?,
        languageVersion: yaml['language_version'] as String?,
        preamble: yaml['preamble'] as String?,
        tsConfig: tsConfig != null
            ? jsonDecode(jsonEncode(tsConfig)) as Map<String, dynamic>
            : null,
        tsConfigFile: yaml['ts_config_file'] as String?,
        functions: FunctionConfig(
            varArgs: (yaml['functions'] as YamlMap?)?['varargs'] as int?),
        includedDeclarations: (yaml['include'] as YamlList?)
                ?.map<String>((node) => node.toString())
                .toList() ??
            [],
        ignoreErrors: yaml['ignore_errors'] as bool? ?? false,
        generateAll: yaml['generate_all'] as bool? ?? false);
  }
}
